本文为学习《深入理解java虚拟机》书中2.4节实战部分的整理总结。

jvm中能引起Out of memory Error 的运行时内存存储区域大致可以分为:

  • java堆区溢出
  • java虚拟机栈和本地方法栈溢出
  • 方法区和运行时常量池溢出
  • 本机直接内存溢出

下面通过示例代码来观察各个区域出错的状况。

一、java堆区溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.ArrayList;
import java.util.List;

/**
* @author yzb
* VM args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapTest {
// 声明一个静态内部类
static class OOMObject{}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
// 不断向list中添加新创建的对象,对象实例会存储在堆中,所以会引起heap space OOM, 错误信息也能证明这一点。
while (true) {
list.add(new OOMObject());
}
}
}
出错信息:
1
2
3
4
5
6
7
8
9
10
11
java.lang.OutOfMemoryError: **Java heap space**
Dumping heap to java_pid5472.hprof ...
Heap dump file created [28155685 bytes in 0.209 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at com.yzb.HeapTest.main(HeapTest.java:17)

基本思路:

java中的对象实例都是储存在堆区中,所以我们通过参数-Xms20m -Xmx20m固定好java堆的大小,并且保证有可达路径来避免GC回收对象,就可以很快将堆区占满,从而发生内存溢出异常,异常信息中的
Java heap space也可以证明这一点。

另一个参数-XX:+HeapDumpOnOutOfMemoryError可以在堆溢出时dump出当前内存堆转储快照,我们用Jprofiler打开快照文件查看内存占用情况:

Name Instance Count Size
com.yzb.HeapTest$OOMObject 810,326 12,965 kB
char[ ] 2,294 313 kB
java.lang.String 2,145 51,480 bytes
java.util.TreeMap$Entry 791 31,640 bytes
java.lang.Object[ ] 583 3,274 kB
java.lang.Class 546 174 kB

可以看到 com.yzb.HeapTest$OOMObject 占用了绝大部分的内存,从而定位出是OOMObject对象实例过多的缘故。

解决方案:

通过内存分析工具分析造成堆溢出的情况。如果是内存泄漏,找到泄漏对象的引用路径,定位对象创建和造成泄漏的位置。如果是内存溢出,检查虚拟机的堆参数-Xmx -Xms 是否还有上调的空间,再从代码上检查是否对象生命周期过长,设计不合理等情况,避免过多的内存消耗。

二、java虚拟机栈和本地方法栈溢出

关于栈溢出,我们可以有两种方法测试:

  • 缩小栈内存容量
  • 增加栈帧长度

我们先看第一种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.yzb;


/**
* @author yzb
* VM args: -Xss128k
*/
public class StackOverFlowTest {
private int stackLength = 1;
private void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable{
StackOverFlowTest soft = new StackOverFlowTest();
try {
soft.stackLeak();
} catch (Throwable e){
System.out.println("stack length: " + soft.stackLength);
throw e;
}
}
}
错误信息:
1
2
3
4
stack length: 983
Exception in thread "main" java.lang.StackOverflowError
at com.yzb.StackOverFlowTest.stackLeak(StackOverFlowTest.java:8)
at com.yzb.StackOverFlowTest.stackLeak(StackOverFlowTest.java:9)

我们通过缩小栈容量,和无止境的递归调用来触发Stack Overflow异常。
这里可以看到,我们在128k的栈容量下,对于stackLeak方法,允许983次递归调用的深度。

再来试试第二种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.yzb;

public class StackOverFlowTest2 {
private int stackLength = 0;
private void test(){
long unused1, unused2, unused3, unused4, unused5,
unused6, unused7, unused8, unused9, unused10,
unused11, unused12, unused13, unused14, unused15,
unused16, unused17, unused18, unused19, unused20,
unused21, unused22, unused23, unused24, unused25,
unused26, unused27, unused28, unused29, unused30,
unused31, unused32, unused33, unused34, unused35,
unused36, unused37, unused38, unused39, unused40,
unused41, unused42, unused43, unused44, unused45,
unused46, unused47, unused48, unused49, unused50,
unused51, unused52, unused53, unused54, unused55,
unused56, unused57, unused58, unused59, unused60,
unused61, unused62, unused63, unused64, unused65,
unused66, unused67, unused68, unused69, unused70,
unused71, unused72, unused73, unused74, unused75,
unused76, unused77, unused78, unused79, unused80,
unused81, unused82, unused83, unused84, unused85,
unused86, unused87, unused88, unused89, unused90,
unused91, unused92, unused93, unused94, unused95,
unused96, unused97, unused98, unused99;
stackLength++;
test();
unused1 = unused2 = unused3 = unused4 = unused5 =
unused6 = unused7 = unused8 = unused9 = unused10 =
unused11 = unused12 = unused13 = unused14 = unused15 =
unused16 = unused17 = unused18 = unused19 = unused20 =
unused21 = unused22 = unused23 = unused24 = unused25 =
unused26 = unused27 = unused28 = unused29 = unused30 =
unused31 = unused32 = unused33 = unused34 = unused35 =
unused36 = unused37 = unused38 = unused39 = unused40 =
unused41 = unused42 = unused43 = unused44 = unused45 =
unused46 = unused47 = unused48 = unused49 = unused50 =
unused51 = unused52 = unused53 = unused54 = unused55 =
unused56 = unused57 = unused58 = unused59 = unused60 =
unused61 = unused62 = unused63 = unused64 = unused65 =
unused66 = unused67 = unused68 = unused69 = unused70 =
unused71 = unused72 = unused73 = unused74 = unused75 =
unused76 = unused77 = unused78 = unused79 = unused80 =
unused81 = unused82 = unused83 = unused84 = unused85 =
unused86 = unused87 = unused88 = unused89 = unused90 =
unused91 = unused92 = unused93 = unused94 = unused95 =
unused96 = unused97 = unused98 = unused99 = 100;

}
public static void main(String[] args) {
StackOverFlowTest2 soft2 = new StackOverFlowTest2();
try {
soft2.test();
} catch (Error e) {
System.out.println("stack length: " + soft2.stackLength);
throw e;
}
}
}
错误信息:
1
2
3
4
stack length: 5209
Exception in thread "main" java.lang.StackOverflowError
at com.yzb.StackOverFlowTest2.test(StackOverFlowTest2.java:27)
at com.yzb.StackOverFlowTest2.test(StackOverFlowTest2.java:27)

现在使用jvm默认的栈大小,并且在方法内定义了100个本地变量来增加每个方法的调用栈帧。

我们看到该方法的调用深度为5209,并且报了StackOverflowError。

解决方案:

对于StackOverflowError, 通常可以设置增加栈容量或者试着将其栈帧变大的方法内部进行削减本地变量,当然更多时候,栈溢出是递归过深造成的,试着减少递归深度或者改写成迭代形式。

三、方法区和运行时常量池溢出

测试运行时常量池溢出,我们立马会想到String,但由于JDK7之后已经将字符串常量池移到了java堆区,所以只能通过限制堆区容量来观察错误信息,而JDK6之前可以通过限制永久代容量来看到错误情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.yzb;

import java.util.HashSet;
import java.util.Set;

/**
* @author yzb
* VM args1: -XX:PermSize=6m -XX:MaxPermSize=6m JDK 6 and before is work.
* VM arg2: -Xms20m -Xmx20m JDK 7 and after is work.
*/
public class RuntimeContantPoolTest {
public static void main(String[] args) {
Set<String> s = new HashSet<>();
int i = 0;
while (true) {
s.add(String.valueOf(i++).intern());
}
}
}
JDK7 的报错信息:
1
2
3
4
5
6
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.HashMap.resize(HashMap.java:704)
at java.util.HashMap.putVal(HashMap.java:663)
at java.util.HashMap.put(HashMap.java:612)
at java.util.HashSet.add(HashSet.java:220)
at com.yzb.RuntimeContantPoolTest.main(RuntimeContantPoolTest.java:16)

JDK8之后,元空间替代了永久代,如果到达了元空间初始空间大小,会触发垃圾收集进行类型卸载,并调整空间大小。

四、本机直接内存溢出

通过反射获取Unsafe实例不断进行内存分配来导致本机直接的内存溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.yzb;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
* @author yzb
*/
public class DirectMemoryTest {
private static final int _1MB = 1024*1024;

public static void main(String[] args) throws Throwable {
int cnt = 0;
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
try{
while (true) {
cnt++;
unsafe.allocateMemory(_1MB);
}
} catch (Error e) {
System.out.println("allocated " + cnt +"MB memory.");
throw e;
}
}
}
报错信息:
1
2
3
4
allocated 13630MB memory.
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.yzb.DirectMemoryTest.main(DirectMemoryTest.java:22)

可以看到在我的机器上申请了13GB的内存后出现了OutOfMemoryError。